/*
 * contrib/pageinspect/btreefuncs.c
 *
 *
 * btreefuncs.c
 *
 * Copyright (c) 2006 Satoshi Nagayasu <nagayasus@nttdata.co.jp>
 *
 * Permission to use, copy, modify, and distribute this software and
 * its documentation for any purpose, without fee, and without a
 * written agreement is hereby granted, provided that the above
 * copyright notice and this paragraph and the following two
 * paragraphs appear in all copies.
 *
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT,
 * INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
 * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
 * DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS
 * IS" BASIS, AND THE AUTHOR HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE,
 * SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 */

#include "postgres.h"

#include "access/nbtree.h"
#include "access/relation.h"
#include "catalog/namespace.h"
#include "catalog/pg_am.h"
#include "catalog/pg_type.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "pageinspect.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/rel.h"
#include "utils/varlena.h"

PG_FUNCTION_INFO_V1(bt_metap);
PG_FUNCTION_INFO_V1(bt_page_items_1_9);
PG_FUNCTION_INFO_V1(bt_page_items);
PG_FUNCTION_INFO_V1(bt_page_items_bytea);
PG_FUNCTION_INFO_V1(bt_page_stats_1_9);
PG_FUNCTION_INFO_V1(bt_page_stats);

#define IS_INDEX(r) ((r)->rd_rel->relkind == RELKIND_INDEX)
#define IS_BTREE(r) ((r)->rd_rel->relam == BTREE_AM_OID)
#define DatumGetItemPointer(X)	 ((ItemPointer) DatumGetPointer(X))
#define ItemPointerGetDatum(X)	 PointerGetDatum(X)

/* 注意：BlockNumber 是无符号的，因此不能为负 */
#define CHECK_RELATION_BLOCK_RANGE(rel, blkno) { \
		if ( RelationGetNumberOfBlocks(rel) <= (BlockNumber) (blkno) ) \
			 elog(ERROR, "block number out of range"); }

/* ------------------------------------------------
 * 单个 btree 页面统计信息的结构
 * ------------------------------------------------
 */
typedef struct BTPageStat
{
	uint32		blkno;
	uint32		live_items;
	uint32		dead_items;
	uint32		page_size;
	uint32		max_avail;
	uint32		free_size;
	uint32		avg_item_size;
	char		type;

	/* 不透明的数据 */
	BlockNumber btpo_prev;
	BlockNumber btpo_next;
	uint32		btpo_level;
	uint16		btpo_flags;
	BTCycleId	btpo_cycleid;
} BTPageStat;


/* -------------------------------------------------
 * GetBTPageStatistics()
 *
 * 收集单个 b-tree 页的统计信息
 * -------------------------------------------------
 */
static void fc_GetBTPageStatistics(BlockNumber fc_blkno, Buffer fc_buffer, BTPageStat *fc_stat)
{
	Page		fc_page = BufferGetPage(fc_buffer);
	PageHeader	fc_phdr = (PageHeader) fc_page;
	OffsetNumber fc_maxoff = PageGetMaxOffsetNumber(fc_page);
	BTPageOpaque fc_opaque = BTPageGetOpaque(fc_page);
	int			fc_item_size = 0;
	int			fc_off;

	fc_stat->blkno = fc_blkno;

	fc_stat->max_avail = BLCKSZ - (BLCKSZ - fc_phdr->pd_special + SizeOfPageHeaderData);

	fc_stat->dead_items = fc_stat->live_items = 0;

	fc_stat->page_size = PageGetPageSize(fc_page);

	/* 页面类型（标志） */
	if (P_ISDELETED(fc_opaque))
	{
		/* 我们将删除的页面分为叶子（'d'）或内部（'D'） */
		if (P_ISLEAF(fc_opaque) || !P_HAS_FULLXID(fc_opaque))
			fc_stat->type = 'd';
		else
			fc_stat->type = 'D';

		/*
		 * 报告删除页面中的 safexid。
		 *
		 * 处理 pg_upgrade 的删除页面，这些页面使用 btpo_level 字段中的
		 * 先前 safexid 表示（这曾经是一个名为“bpto”的联合类型）。
		 */
		if (P_HAS_FULLXID(fc_opaque))
		{
			FullTransactionId fc_safexid = BTPageGetDeleteXid(fc_page);

			elog(DEBUG2, "deleted page from block %u has safexid %u:%u",
				 fc_blkno, EpochFromFullTransactionId(fc_safexid),
				 XidFromFullTransactionId(fc_safexid));
		}
		else
			elog(DEBUG2, "deleted page from block %u has safexid %u",
				 fc_blkno, fc_opaque->btpo_level);

		/* 不要将 BTDeletedPageData 解释为索引元组 */
		fc_maxoff = InvalidOffsetNumber;
	}
	else if (P_IGNORE(fc_opaque))
		fc_stat->type = 'e';
	else if (P_ISLEAF(fc_opaque))
		fc_stat->type = 'l';
	else if (P_ISROOT(fc_opaque))
		fc_stat->type = 'r';
	else
		fc_stat->type = 'i';

	/* btpage 不透明数据 */
	fc_stat->btpo_prev = fc_opaque->btpo_prev;
	fc_stat->btpo_next = fc_opaque->btpo_next;
	fc_stat->btpo_level = fc_opaque->btpo_level;
	fc_stat->btpo_flags = fc_opaque->btpo_flags;
	fc_stat->btpo_cycleid = fc_opaque->btpo_cycleid;

	/* 计算活动和死亡元组以及自由空间 */
	for (fc_off = FirstOffsetNumber; fc_off <= fc_maxoff; fc_off++)
	{
		IndexTuple	fc_itup;

		ItemId		fc_id = PageGetItemId(fc_page, fc_off);

		fc_itup = (IndexTuple) PageGetItem(fc_page, fc_id);

		fc_item_size += IndexTupleSize(fc_itup);

		if (!ItemIdIsDead(fc_id))
			fc_stat->live_items++;
		else
			fc_stat->dead_items++;
	}
	fc_stat->free_size = PageGetFreeSpace(fc_page);

	if ((fc_stat->live_items + fc_stat->dead_items) > 0)
		fc_stat->avg_item_size = fc_item_size / (fc_stat->live_items + fc_stat->dead_items);
	else
		fc_stat->avg_item_size = 0;
}

/* -----------------------------------------------
 * bt_page_stats()
 *
 * 使用：SELECT * FROM bt_page_stats('t1_pkey', 1);
 * -----------------------------------------------
 */
static Datum fc_bt_page_stats_internal(PG_FUNCTION_ARGS, enum pageinspect_version fc_ext_version)
{
	text	   *fc_relname = PG_GETARG_TEXT_PP(0);
	int64		fc_blkno = (fc_ext_version == PAGEINSPECT_V1_8 ? PG_GETARG_UINT32(1) : PG_GETARG_INT64(1));
	Buffer		fc_buffer;
	Relation	fc_rel;
	RangeVar   *fc_relrv;
	Datum		fc_result;
	HeapTuple	fc_tuple;
	TupleDesc	fc_tupleDesc;
	int			fc_j;
	char	   *fc_values[11];
	BTPageStat	fc_stat;

	if (!superuser())
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 errmsg("must be superuser to use pageinspect functions")));

	fc_relrv = makeRangeVarFromNameList(textToQualifiedNameList(fc_relname));
	fc_rel = relation_openrv(fc_relrv, AccessShareLock);

	if (!IS_INDEX(fc_rel) || !IS_BTREE(fc_rel))
		ereport(ERROR,
				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
				 errmsg("\"%s\" is not a %s index",
						RelationGetRelationName(fc_rel), "btree")));

	/*
	 * 拒绝尝试读取非本地临时关系；我们可能会获取错误的数据，因为
	 * 我们无法查看拥有会话的本地缓冲区。
	 */
	if (RELATION_IS_OTHER_TEMP(fc_rel))
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot access temporary tables of other sessions")));

	if (fc_blkno < 0 || fc_blkno > MaxBlockNumber)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("invalid block number")));

	if (fc_blkno == 0)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("block 0 is a meta page")));

	CHECK_RELATION_BLOCK_RANGE(fc_rel, fc_blkno);

	fc_buffer = ReadBuffer(fc_rel, fc_blkno);
	LockBuffer(fc_buffer, BUFFER_LOCK_SHARE);

	/* 保持编译器安静 */
	fc_stat.btpo_prev = fc_stat.btpo_next = InvalidBlockNumber;
	fc_stat.btpo_flags = fc_stat.free_size = fc_stat.avg_item_size = 0;

	fc_GetBTPageStatistics(fc_blkno, fc_buffer, &fc_stat);

	UnlockReleaseBuffer(fc_buffer);
	relation_close(fc_rel, AccessShareLock);

	/* 为我们的结果类型构建一个元组描述符 */
	if (get_call_result_type(fcinfo, NULL, &fc_tupleDesc) != TYPEFUNC_COMPOSITE)
		elog(ERROR, "return type must be a row type");

	fc_j = 0;
	fc_values[fc_j++] = psprintf("%u", fc_stat.blkno);
	fc_values[fc_j++] = psprintf("%c", fc_stat.type);
	fc_values[fc_j++] = psprintf("%u", fc_stat.live_items);
	fc_values[fc_j++] = psprintf("%u", fc_stat.dead_items);
	fc_values[fc_j++] = psprintf("%u", fc_stat.avg_item_size);
	fc_values[fc_j++] = psprintf("%u", fc_stat.page_size);
	fc_values[fc_j++] = psprintf("%u", fc_stat.free_size);
	fc_values[fc_j++] = psprintf("%u", fc_stat.btpo_prev);
	fc_values[fc_j++] = psprintf("%u", fc_stat.btpo_next);
	fc_values[fc_j++] = psprintf("%u", fc_stat.btpo_level);
	fc_values[fc_j++] = psprintf("%d", fc_stat.btpo_flags);

	fc_tuple = BuildTupleFromCStrings(TupleDescGetAttInMetadata(fc_tupleDesc),
								   fc_values);

	fc_result = HeapTupleGetDatum(fc_tuple);

	PG_RETURN_DATUM(fc_result);
}

Datum bt_page_stats_1_9(PG_FUNCTION_ARGS)
{
	return fc_bt_page_stats_internal(fcinfo, PAGEINSPECT_V1_9);
}

/* 旧扩展版本的入口点 */
Datum bt_page_stats(PG_FUNCTION_ARGS)
{
	return fc_bt_page_stats_internal(fcinfo, PAGEINSPECT_V1_8);
}


/*
 * SRF 的跨调用数据结构
 */
struct user_args
{
	Page		page;
	OffsetNumber offset;
	bool		leafpage;
	bool		rightmost;
	TupleDesc	tupd;
};

/*-------------------------------------------------------
 * bt_page_print_tuples()
 *
 * 形成描述给定偏移量处索引元组的元组
 * ------------------------------------------------------
 */
static Datum fc_bt_page_print_tuples(struct user_args *fc_uargs)
{
	Page		fc_page = fc_uargs->page;
	OffsetNumber fc_offset = fc_uargs->offset;
	bool		fc_leafpage = fc_uargs->leafpage;
	bool		fc_rightmost = fc_uargs->rightmost;
	bool		fc_ispivottuple;
	Datum		fc_values[9];
	bool		fc_nulls[9];
	HeapTuple	fc_tuple;
	ItemId		fc_id;
	IndexTuple	fc_itup;
	int			fc_j;
	int			fc_off;
	int			fc_dlen;
	char	   *fc_dump,
			   *fc_datacstring;
	char	   *fc_ptr;
	ItemPointer fc_htid;

	fc_id = PageGetItemId(fc_page, fc_offset);

	if (!ItemIdIsValid(fc_id))
		elog(ERROR, "invalid ItemId");

	fc_itup = (IndexTuple) PageGetItem(fc_page, fc_id);

	fc_j = 0;
	memset(fc_nulls, 0, sizeof(fc_nulls));
	fc_values[fc_j++] = DatumGetInt16(fc_offset);
	fc_values[fc_j++] = ItemPointerGetDatum(&fc_itup->t_tid);
	fc_values[fc_j++] = Int32GetDatum((int) IndexTupleSize(fc_itup));
	fc_values[fc_j++] = BoolGetDatum(IndexTupleHasNulls(fc_itup));
	fc_values[fc_j++] = BoolGetDatum(IndexTupleHasVarwidths(fc_itup));

	fc_ptr = (char *) fc_itup + IndexInfoFindDataOffset(fc_itup->t_info);
	fc_dlen = IndexTupleSize(fc_itup) - IndexInfoFindDataOffset(fc_itup->t_info);

	/*
	 * 确保“data”列不包括发布列表或堆 TID 的枢轴
	 * 元组表示。
	 *
	 * 注意：BTreeTupleIsPivot() 在非 heapkeyspace 索引上不会可靠工作
	 * （在 BTREE_VERSION 4 之前构建的那些），但我们无法确定此页面
	 * 是否来自非 heapkeyspace 索引。我们可能只有一个 bytea
	 * nbtree 页面图像可供参考，因此通常没有元页面可以检查。
	 *
	 * 在这里没问题，因为 BTreeTupleIsPivot() 只会返回非 heapkeyspace
	 * 枢轴的 false，绝不会对非 heapkeyspace 非枢轴返回 true。
	 * 由于堆 TID 无论如何不是非 heapkeyspace 索引中的键空间的一部分，
	 * 因此不可能有未调整的枢轴元组堆 TID 表示。非 heapkeyspace
	 * 索引可以使 BTreeTupleIsPivot() 返回 true（由于像 Postgres
	 * v11 中 INCLUDE 索引的后缀截断等事情），但是当发生这种情况时，
	 * BTreeTupleGetHeapTID() 可以被信赖来可靠工作（即返回 NULL）。
	 *
	 * 注意：BTreeTupleIsPosting() 始终可靠工作，即使在
	 * 非 heapkeyspace 索引中。
	 */
	if (BTreeTupleIsPosting(fc_itup))
		fc_dlen -= IndexTupleSize(fc_itup) - BTreeTupleGetPostingOffset(fc_itup);
	else if (BTreeTupleIsPivot(fc_itup) && BTreeTupleGetHeapTID(fc_itup) != NULL)
		fc_dlen -= MAXALIGN(sizeof(ItemPointerData));

	if (fc_dlen < 0 || fc_dlen > INDEX_SIZE_MASK)
		elog(ERROR, "invalid tuple length %d for tuple at offset number %u",
			 fc_dlen, fc_offset);
	fc_dump = palloc0(fc_dlen * 3 + 1);
	fc_datacstring = fc_dump;
	for (fc_off = 0; fc_off < fc_dlen; fc_off++)
	{
		if (fc_off > 0)
			*fc_dump++ = ' ';
		sprintf(fc_dump, "%02x", *(fc_ptr + fc_off) & 0xff);
		fc_dump += 2;
	}
	fc_values[fc_j++] = CStringGetTextDatum(fc_datacstring);
	pfree(fc_datacstring);

	/*
	 * 我们需要再次解决 BTreeTupleIsPivot() !heapkeyspace 限制
	 * 根据元组是否必须是中心元组来推断，依据因素是页面是否是叶页面，以及元组的页面偏移量编号。
	 */
	fc_ispivottuple = (!fc_leafpage || (!fc_rightmost && fc_offset == P_HIKEY));

	/* LP_DEAD 位在中心元组中永远不能被设置，因此在那里显示 NULL */
	if (!fc_ispivottuple)
		fc_values[fc_j++] = BoolGetDatum(ItemIdIsDead(fc_id));
	else
	{
		Assert(!ItemIdIsDead(fc_id));
		fc_nulls[fc_j++] = true;
	}

	fc_htid = BTreeTupleGetHeapTID(fc_itup);
	if (fc_ispivottuple && !BTreeTupleIsPivot(fc_itup))
	{
		/* 不要在 !heapkeyspace 中的中心元组中显示虚假的堆 TID */
		fc_htid = NULL;
	}

	if (fc_htid)
		fc_values[fc_j++] = ItemPointerGetDatum(fc_htid);
	else
		fc_nulls[fc_j++] = true;

	if (BTreeTupleIsPosting(fc_itup))
	{
		/* 构建项目指针数组 */
		ItemPointer fc_tids;
		Datum	   *fc_tids_datum;
		int			fc_nposting;

		fc_tids = BTreeTupleGetPosting(fc_itup);
		fc_nposting = BTreeTupleGetNPosting(fc_itup);
		fc_tids_datum = (Datum *) palloc(fc_nposting * sizeof(Datum));
		for (int fc_i = 0; fc_i < fc_nposting; fc_i++)
			fc_tids_datum[fc_i] = ItemPointerGetDatum(&fc_tids[fc_i]);
		fc_values[fc_j++] = PointerGetDatum(construct_array(fc_tids_datum,
													  fc_nposting,
													  TIDOID,
													  sizeof(ItemPointerData),
													  false, TYPALIGN_SHORT));
		pfree(fc_tids_datum);
	}
	else
		fc_nulls[fc_j++] = true;

	/* 构建并返回结果元组 */
	fc_tuple = heap_form_tuple(fc_uargs->tupd, fc_values, fc_nulls);

	return HeapTupleGetDatum(fc_tuple);
}

/*-------------------------------------------------------
 * bt_page_items()
 *
 * 获取 btree 页面中的 IndexTupleData 集合
 *
 * 用法：SELECT * FROM bt_page_items('t1_pkey', 1);
 *-------------------------------------------------------
 */
static Datum fc_bt_page_items_internal(PG_FUNCTION_ARGS, enum pageinspect_version fc_ext_version)
{
	text	   *fc_relname = PG_GETARG_TEXT_PP(0);
	int64		fc_blkno = (fc_ext_version == PAGEINSPECT_V1_8 ? PG_GETARG_UINT32(1) : PG_GETARG_INT64(1));
	Datum		fc_result;
	FuncCallContext *fc_fctx;
	MemoryContext fc_mctx;
	struct user_args *fc_uargs;

	if (!superuser())
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 errmsg("must be superuser to use pageinspect functions")));

	if (SRF_IS_FIRSTCALL())
	{
		RangeVar   *fc_relrv;
		Relation	fc_rel;
		Buffer		fc_buffer;
		BTPageOpaque fc_opaque;
		TupleDesc	fc_tupleDesc;

		fc_fctx = SRF_FIRSTCALL_INIT();

		fc_relrv = makeRangeVarFromNameList(textToQualifiedNameList(fc_relname));
		fc_rel = relation_openrv(fc_relrv, AccessShareLock);

		if (!IS_INDEX(fc_rel) || !IS_BTREE(fc_rel))
			ereport(ERROR,
					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
					 errmsg("\"%s\" is not a %s index",
							RelationGetRelationName(fc_rel), "btree")));

		/*
		 * 拒绝尝试读取非本地临时关系；我们很可能得到错误的数据，因为我们无法查看拥有会话的本地缓冲区。
		 */
		if (RELATION_IS_OTHER_TEMP(fc_rel))
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("cannot access temporary tables of other sessions")));

		if (fc_blkno < 0 || fc_blkno > MaxBlockNumber)
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("invalid block number")));

		if (fc_blkno == 0)
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("block 0 is a meta page")));

		CHECK_RELATION_BLOCK_RANGE(fc_rel, fc_blkno);

		fc_buffer = ReadBuffer(fc_rel, fc_blkno);
		LockBuffer(fc_buffer, BUFFER_LOCK_SHARE);

		/*
		 * 我们将页面复制到本地存储中，以避免在缓冲区上保持锁定时间过长，并且如果调用查询没有获取所有行，可能根本无法释放它。
		 */
		fc_mctx = MemoryContextSwitchTo(fc_fctx->multi_call_memory_ctx);

		fc_uargs = palloc(sizeof(struct user_args));

		fc_uargs->page = palloc(BLCKSZ);
		memcpy(fc_uargs->page, BufferGetPage(fc_buffer), BLCKSZ);

		UnlockReleaseBuffer(fc_buffer);
		relation_close(fc_rel, AccessShareLock);

		fc_uargs->offset = FirstOffsetNumber;

		fc_opaque = BTPageGetOpaque(fc_uargs->page);

		if (!P_ISDELETED(fc_opaque))
			fc_fctx->max_calls = PageGetMaxOffsetNumber(fc_uargs->page);
		else
		{
			/* 不要将 BTDeletedPageData 解释为索引元组 */
			elog(NOTICE, "page from block " INT64_FORMAT " is deleted", fc_blkno);
			fc_fctx->max_calls = 0;
		}
		fc_uargs->leafpage = P_ISLEAF(fc_opaque);
		fc_uargs->rightmost = P_RIGHTMOST(fc_opaque);

		/* 为我们的结果类型构建一个元组描述符 */
		if (get_call_result_type(fcinfo, NULL, &fc_tupleDesc) != TYPEFUNC_COMPOSITE)
			elog(ERROR, "return type must be a row type");
		fc_tupleDesc = BlessTupleDesc(fc_tupleDesc);

		fc_uargs->tupd = fc_tupleDesc;

		fc_fctx->user_fctx = fc_uargs;

		MemoryContextSwitchTo(fc_mctx);
	}

	fc_fctx = SRF_PERCALL_SETUP();
	fc_uargs = fc_fctx->user_fctx;

	if (fc_fctx->call_cntr < fc_fctx->max_calls)
	{
		fc_result = fc_bt_page_print_tuples(fc_uargs);
		fc_uargs->offset++;
		SRF_RETURN_NEXT(fc_fctx, fc_result);
	}

	SRF_RETURN_DONE(fc_fctx);
}

Datum bt_page_items_1_9(PG_FUNCTION_ARGS)
{
	return fc_bt_page_items_internal(fcinfo, PAGEINSPECT_V1_9);
}

/* 旧扩展版本的入口点 */
Datum bt_page_items(PG_FUNCTION_ARGS)
{
	return fc_bt_page_items_internal(fcinfo, PAGEINSPECT_V1_8);
}

/*-------------------------------------------------------
 * bt_page_items_bytea()
 *
 * 获取 btree 页面中的 IndexTupleData 集合
 *
 * 用法：SELECT * FROM bt_page_items(get_raw_page('t1_pkey', 1));
 *-------------------------------------------------------
 */

Datum bt_page_items_bytea(PG_FUNCTION_ARGS)
{
	bytea	   *fc_raw_page = PG_GETARG_BYTEA_P(0);
	Datum		fc_result;
	FuncCallContext *fc_fctx;
	struct user_args *fc_uargs;

	if (!superuser())
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 errmsg("must be superuser to use raw page functions")));

	if (SRF_IS_FIRSTCALL())
	{
		BTPageOpaque fc_opaque;
		MemoryContext fc_mctx;
		TupleDesc	fc_tupleDesc;

		fc_fctx = SRF_FIRSTCALL_INIT();
		fc_mctx = MemoryContextSwitchTo(fc_fctx->multi_call_memory_ctx);

		fc_uargs = palloc(sizeof(struct user_args));

		fc_uargs->page = get_page_from_raw(fc_raw_page);

		if (PageIsNew(fc_uargs->page))
		{
			MemoryContextSwitchTo(fc_mctx);
			PG_RETURN_NULL();
		}

		fc_uargs->offset = FirstOffsetNumber;

		/* 验证特殊空间的大小是否符合预期 */
		if (PageGetSpecialSize(fc_uargs->page) != MAXALIGN(sizeof(BTPageOpaqueData)))
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("input page is not a valid %s page", "btree"),
					 errdetail("Expected special size %d, got %d.",
							   (int) MAXALIGN(sizeof(BTPageOpaqueData)),
							   (int) PageGetSpecialSize(fc_uargs->page))));

		fc_opaque = BTPageGetOpaque(fc_uargs->page);

		if (P_ISMETA(fc_opaque))
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("block is a meta page")));

		if (P_ISLEAF(fc_opaque) && fc_opaque->btpo_level != 0)
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("block is not a valid btree leaf page")));

		if (P_ISDELETED(fc_opaque))
			elog(NOTICE, "page is deleted");

		if (!P_ISDELETED(fc_opaque))
			fc_fctx->max_calls = PageGetMaxOffsetNumber(fc_uargs->page);
		else
		{
			/* 不要将 BTDeletedPageData 解释为索引元组 */
			elog(NOTICE, "page from block is deleted");
			fc_fctx->max_calls = 0;
		}
		fc_uargs->leafpage = P_ISLEAF(fc_opaque);
		fc_uargs->rightmost = P_RIGHTMOST(fc_opaque);

		/* 为我们的结果类型构建一个元组描述符 */
		if (get_call_result_type(fcinfo, NULL, &fc_tupleDesc) != TYPEFUNC_COMPOSITE)
			elog(ERROR, "return type must be a row type");
		fc_tupleDesc = BlessTupleDesc(fc_tupleDesc);

		fc_uargs->tupd = fc_tupleDesc;

		fc_fctx->user_fctx = fc_uargs;

		MemoryContextSwitchTo(fc_mctx);
	}

	fc_fctx = SRF_PERCALL_SETUP();
	fc_uargs = fc_fctx->user_fctx;

	if (fc_fctx->call_cntr < fc_fctx->max_calls)
	{
		fc_result = fc_bt_page_print_tuples(fc_uargs);
		fc_uargs->offset++;
		SRF_RETURN_NEXT(fc_fctx, fc_result);
	}

	SRF_RETURN_DONE(fc_fctx);
}

/* bt_metap() 的输出参数（列）数量 */
#define BT_METAP_COLS_V1_8		9

/* ------------------------------------------------
 * bt_metap()
 *
 * 获取 btree 的元页面信息
 *
 * 用法：SELECT * FROM bt_metap('t1_pkey')
 * ------------------------------------------------
 */
Datum bt_metap(PG_FUNCTION_ARGS)
{
	text	   *fc_relname = PG_GETARG_TEXT_PP(0);
	Datum		fc_result;
	Relation	fc_rel;
	RangeVar   *fc_relrv;
	BTMetaPageData *fc_metad;
	TupleDesc	fc_tupleDesc;
	int			fc_j;
	char	   *fc_values[9];
	Buffer		fc_buffer;
	Page		fc_page;
	HeapTuple	fc_tuple;

	if (!superuser())
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 errmsg("must be superuser to use pageinspect functions")));

	fc_relrv = makeRangeVarFromNameList(textToQualifiedNameList(fc_relname));
	fc_rel = relation_openrv(fc_relrv, AccessShareLock);

	if (!IS_INDEX(fc_rel) || !IS_BTREE(fc_rel))
		ereport(ERROR,
				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
				 errmsg("\"%s\" is not a %s index",
						RelationGetRelationName(fc_rel), "btree")));

	/*
	 * 拒绝尝试读取非本地临时关系；我们可能会获取错误的数据，因为
	 * 我们无法查看拥有会话的本地缓冲区。
	 */
	if (RELATION_IS_OTHER_TEMP(fc_rel))
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot access temporary tables of other sessions")));

	fc_buffer = ReadBuffer(fc_rel, 0);
	LockBuffer(fc_buffer, BUFFER_LOCK_SHARE);

	fc_page = BufferGetPage(fc_buffer);
	fc_metad = BTPageGetMeta(fc_page);

	/* 为我们的结果类型构建一个元组描述符 */
	if (get_call_result_type(fcinfo, NULL, &fc_tupleDesc) != TYPEFUNC_COMPOSITE)
		elog(ERROR, "return type must be a row type");

	/*
	 * 我们需要一个权宜之计来检测 1.8 版本之前的 API 版本。
	 * 早期版本错误地为某些列使用了 int4。
	 *
	 * 此时没有可靠的方法来避免旧函数定义所产生的问题，因此坚持用户更新扩展。
	 */
	if (fc_tupleDesc->natts < BT_METAP_COLS_V1_8)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
				 errmsg("function has wrong number of declared columns"),
				 errhint("To resolve the problem, update the \"pageinspect\" extension to the latest version.")));

	fc_j = 0;
	fc_values[fc_j++] = psprintf("%d", fc_metad->btm_magic);
	fc_values[fc_j++] = psprintf("%d", fc_metad->btm_version);
	fc_values[fc_j++] = psprintf(INT64_FORMAT, (int64) fc_metad->btm_root);
	fc_values[fc_j++] = psprintf(INT64_FORMAT, (int64) fc_metad->btm_level);
	fc_values[fc_j++] = psprintf(INT64_FORMAT, (int64) fc_metad->btm_fastroot);
	fc_values[fc_j++] = psprintf(INT64_FORMAT, (int64) fc_metad->btm_fastlevel);

	/*
	 * 如果有可用的扩展元数据值请获取，否则使用默认值。
	 * 注意，我们依赖于假设 btm_allequalimage 已在 Postgres 13 之前构建的索引中初始化为零（就像 _bt_metaversion() 一样）。
	 */
	if (fc_metad->btm_version >= BTREE_NOVAC_VERSION)
	{
		fc_values[fc_j++] = psprintf(INT64_FORMAT,
							   (int64) fc_metad->btm_last_cleanup_num_delpages);
		fc_values[fc_j++] = psprintf("%f", fc_metad->btm_last_cleanup_num_heap_tuples);
		fc_values[fc_j++] = fc_metad->btm_allequalimage ? "t" : "f";
	}
	else
	{
		fc_values[fc_j++] = "0";
		fc_values[fc_j++] = "-1";
		fc_values[fc_j++] = "f";
	}

	fc_tuple = BuildTupleFromCStrings(TupleDescGetAttInMetadata(fc_tupleDesc),
								   fc_values);

	fc_result = HeapTupleGetDatum(fc_tuple);

	UnlockReleaseBuffer(fc_buffer);
	relation_close(fc_rel, AccessShareLock);

	PG_RETURN_DATUM(fc_result);
}
